๐Ÿ”’

POS System

Silakan Masuk

Nama Pengguna
PIN Keamanan

Akses terbatas untuk pegawai aktif.

Menu Penjualan

Memuat menu...

`; Swal.close(); let printWindow = window.open('', '_blank', 'width=300,height=500'); printWindow.document.write(strukHTML); printWindow.document.close(); printWindow.focus(); setTimeout(() => { printWindow.print(); printWindow.close(); }, 500); } // =========================================================== // KASIR (POS) & CHECKOUT (lewat RPC proses_checkout) // =========================================================== let keranjang = []; let dataProdukGlobal = []; let kategoriAktifKasir = 'Semua'; async function muatLayarKasir() { const { data, error } = await supabaseClient.from('master_produk') .select('id_produk, nama_produk, kategori, harga_jual, gambar_url') .eq('aktif', true).order('kategori'); if (error) { NotifikasiKecil(pesanError(error), 'error'); return; } dataProdukGlobal = data || []; renderProdukKasir(); } function setKategoriKasir(kategoriPilihan, elemenTombol) { kategoriAktifKasir = kategoriPilihan; document.querySelectorAll('.btn-kategori').forEach(btn => { btn.className = 'btn-kategori px-4 py-2 rounded-full text-[10px] md:text-xs font-bold whitespace-nowrap bg-white text-slate-500 border border-slate-200 hover:bg-slate-100 transition-colors shadow-sm'; }); elemenTombol.className = 'btn-kategori px-4 py-2 rounded-full text-[10px] md:text-xs font-bold whitespace-nowrap bg-slate-800 text-amber-400 transition-colors shadow-sm'; renderProdukKasir(); } function renderProdukKasir() { const area = document.getElementById('area-produk'); const keyword = document.getElementById('search-produk').value.toLowerCase(); const kategori = kategoriAktifKasir; let produkTerfilter = dataProdukGlobal.filter(p => { let cocokKategori = (kategori === 'Semua') || (p.kategori === kategori); let cocokKata = (p.nama_produk || '').toLowerCase().includes(keyword); return cocokKategori && cocokKata; }); if (produkTerfilter.length === 0) { area.innerHTML = `
๐Ÿ”

Menu tidak ditemukan

`; return; } let html = ''; produkTerfilter.forEach(p => { let visual = p.gambar_url && p.gambar_url.startsWith('http') ? `` : `
${esc(p.gambar_url || '๐Ÿ”')}
`; html += `
${visual}

${esc(p.kategori)}

${esc(p.nama_produk)}

Rp ${(p.harga_jual||0).toLocaleString('id-ID')}

`; }); area.innerHTML = html; } // Harga diambil dari data server (bukan dari atribut) โ†’ tak bisa dimanipulasi function klikProduk(id) { const p = dataProdukGlobal.find(x => x.id_produk === id); if (!p) return; let item = keranjang.find(i => i.id === id); if (item) { item.qty += 1; item.subtotal = item.qty * item.harga; } else keranjang.push({ id, nama: p.nama_produk, harga: p.harga_jual, qty: 1, subtotal: p.harga_jual }); renderKeranjang(); } function renderKeranjang() { const area = document.getElementById('area-keranjang'); const btnBayar = document.getElementById('btn-bayar'); if (keranjang.length === 0) { area.innerHTML = '
Kosong
'; document.getElementById('teks-total').innerText = 'Rp 0'; btnBayar.innerText = 'PILIH MENU'; btnBayar.className = 'w-full bg-slate-300 text-slate-500 font-bold py-3 md:py-4 rounded-xl cursor-not-allowed'; return; } let html = '', total = 0; keranjang.forEach((item, index) => { total += item.subtotal; html += `

${esc(item.nama)}

${item.qty} x ${item.harga.toLocaleString('id-ID')}

Rp ${item.subtotal.toLocaleString('id-ID')}

`; }); area.innerHTML = html; document.getElementById('teks-total').innerText = 'Rp ' + total.toLocaleString('id-ID'); btnBayar.innerText = 'BAYAR Rp ' + total.toLocaleString('id-ID'); btnBayar.className = 'w-full bg-amber-500 hover:bg-amber-600 text-white font-bold py-3 md:py-4 rounded-xl shadow-md cursor-pointer active:scale-95 transition-transform'; } window.hapusItemKeranjang = function(index) { keranjang.splice(index, 1); renderKeranjang(); }; function kosongkanKeranjang() { keranjang = []; renderKeranjang(); } function prosesBayar() { if (keranjang.length === 0) return; let totalB = keranjang.reduce((s, i) => s + i.subtotal, 0); let n1 = Math.ceil(totalB / 10000) * 10000; let n2 = Math.ceil(totalB / 50000) * 50000; let n3 = Math.ceil(totalB / 100000) * 100000; let n4 = Math.ceil(totalB / 100000) * 100000 + 50000; let listNominal = [...new Set([totalB, n1, n2, n3, n4])].filter(n => n >= totalB).sort((a,b)=>a-b); let btnNominals = listNominal.map(n => ``).join(''); window.metodeTerpilih = 'Tunai'; window.totalTagihan = totalB; window.setMetode = (metode) => { window.metodeTerpilih = metode; ['Tunai', 'QRIS', 'CardTF'].forEach(m => { let btn = document.getElementById('btn-' + m); if (btn) { btn.className = (m === metode.replace('/','')) ? 'flex-1 bg-amber-500 text-white py-2 rounded-lg font-bold border-2 border-amber-500 shadow-md transition text-xs md:text-sm' : 'flex-1 bg-white text-slate-500 py-2 rounded-lg font-bold border-2 border-slate-200 transition text-xs md:text-sm hover:bg-slate-50'; } }); document.getElementById('area-cash').style.display = (metode === 'Tunai') ? 'block' : 'none'; }; window.setNominal = (nom) => { document.getElementById('input-nominal').value = nom.toLocaleString('id-ID'); window.hitungKembalian(); }; window.hitungKembalian = () => { let dibayar = parseInt(document.getElementById('input-nominal').value.replace(/\./g, '')) || 0; let kembali = dibayar - window.totalTagihan; let el = document.getElementById('teks-kembalian'); if (kembali < 0) { el.innerText = "Uang Kurang!"; el.className = "text-sm md:text-lg font-bold text-red-500 bg-red-50 px-2 py-1 rounded"; } else { el.innerText = "Rp " + kembali.toLocaleString('id-ID'); el.className = "text-lg md:text-xl font-bold text-emerald-600"; } }; Swal.fire({ title: '', width: 450, showCancelButton: true, confirmButtonText: 'Selesaikan Transaksi', cancelButtonText: 'Batal', confirmButtonColor: '#10b981', cancelButtonColor: '#94a3b8', html: `

Total Tagihan

Rp ${totalB.toLocaleString('id-ID')}

${btnNominals}
KembalianRp 0
`, preConfirm: () => { let dibayar = window.totalTagihan; if (window.metodeTerpilih === 'Tunai') { dibayar = parseInt(document.getElementById('input-nominal').value.replace(/\./g, '')) || 0; if (dibayar < window.totalTagihan) { Swal.showValidationMessage('Uang yang dibayarkan kurang dari total tagihan!'); return false; } } return { metode: window.metodeTerpilih, dibayar: dibayar }; } }).then(async (result) => { if (!result.isConfirmed) return; Swal.fire({ title: 'Memproses Pembayaran...', allowOutsideClick: false, didOpen: () => Swal.showLoading() }); // UUID unik per percobaan bayar โ†’ mencegah transaksi ganda const refKlien = (crypto && crypto.randomUUID) ? crypto.randomUUID() : ('r-' + Date.now() + '-' + Math.random().toString(36).slice(2)); const { data, error } = await supabaseClient.rpc('proses_checkout', { p_items: keranjang.map(i => ({ id_produk: i.id, qty: i.qty })), p_metode: result.value.metode, p_dibayar: result.value.dibayar, p_ref_klien: refKlien }); if (error) { return Swal.fire('Transaksi Gagal', pesanError(error, 'Gagal menyimpan transaksi'), 'error'); } let idTrx = data.id_transaksi; let kembalian = data.kembalian || 0; let teksKembali = (result.value.metode === 'Tunai' && kembalian > 0) ? `

Kembalian: Rp ${kembalian.toLocaleString('id-ID')}` : ''; kosongkanKeranjang(); Swal.fire({ title: 'Transaksi Berhasil!', html: `No. Struk: ${esc(idTrx)}
Metode: ${esc(result.value.metode)} ${teksKembali}`, icon: 'success', showDenyButton: true, confirmButtonText: 'Menu Utama', denyButtonText: '๐Ÿ–จ๏ธ Cetak Struk', confirmButtonColor: '#0f172a', denyButtonColor: '#10b981' }).then((res) => { if (res.isDenied) cetakStruk(idTrx); }); }); } // =========================================================== // MANAJEMEN PENGGUNA & HAK AKSES (lewat Edge Function) // =========================================================== const LABEL_MENU = { 'layar-kasir':'Kasir', 'layar-produk':'Produk', 'layar-bahan':'Bahan', 'layar-transaksi':'Transaksi', 'layar-pengeluaran':'Kas Keluar', 'layar-pengguna':'Pengguna', 'layar-pengaturan':'Pengaturan' }; async function ambilDataPengguna() { const { data, error } = await supabaseClient.from('pengguna') .select('id_user, username, nama_lengkap, role, akses_menu, status_aktif').order('role'); if (error) { document.getElementById('area-card-pengguna').innerHTML = `
${esc(pesanError(error))}
`; return; } let html = ''; (data || []).forEach(u => { let aksesArr = Array.isArray(u.akses_menu) ? u.akses_menu : []; let badgeAkses = aksesArr.map(a => `${esc(LABEL_MENU[a] || a.replace('layar-',''))}`).join(' '); let statusColor = u.status_aktif ? 'bg-emerald-100 text-emerald-700' : 'bg-red-100 text-red-700'; let statusText = u.status_aktif ? 'Aktif' : 'Diblokir'; let barWarna = u.role === 'Owner' ? 'bg-amber-400' : (u.role === 'Admin' ? 'bg-indigo-500' : 'bg-slate-400'); let diriSendiri = window.currentUser && window.currentUser.id_user === u.id_user; html += `
${esc((u.nama_lengkap||'?').charAt(0).toUpperCase())}

${esc(u.nama_lengkap)} ${diriSendiri ? '(Anda)' : ''}

@${esc(u.username)} ยท ${esc(u.role)}

${statusText}

Hak Akses Menu:

${badgeAkses || 'Tidak ada akses'}
${diriSendiri ? '' : ``}
`; }); document.getElementById('area-card-pengguna').innerHTML = html || '
Belum ada data pegawai.
'; } // Pemanggil Edge Function (menyertakan token login) async function panggilFungsiPegawai(payload) { const { data, error } = await supabaseClient.functions.invoke(FUNGSI_PEGAWAI, { body: payload }); if (error) { // Coba baca pesan dari body respons (FunctionsHttpError) let pesan = 'Gagal menghubungi server'; try { const j = await error.context.json(); pesan = j.pesan || pesan; } catch(e) { pesan = error.message || pesan; } return { ok: false, pesan }; } return data; } async function toggleStatusPegawai(id_user, jadikanAktif, nama) { const konfirmasi = await Swal.fire({ title: jadikanAktif === true || jadikanAktif === 'true' ? 'Aktifkan Pegawai?' : 'Blokir Pegawai?', text: `${nama} ${(jadikanAktif === true || jadikanAktif === 'true') ? 'akan bisa login kembali.' : 'tidak akan bisa login.'}`, icon: 'warning', showCancelButton: true, confirmButtonColor: '#ef4444', confirmButtonText: 'Ya' }); if (!konfirmasi.isConfirmed) return; Swal.fire({ title: 'Memproses...', allowOutsideClick: false, didOpen: () => Swal.showLoading() }); const hasil = await panggilFungsiPegawai({ aksi: 'ubah', id_user, status_aktif: (jadikanAktif === true || jadikanAktif === 'true') }); if (!hasil.ok) return Swal.fire('Gagal', hasil.pesan, 'error'); Swal.fire('Berhasil', 'Status pegawai diperbarui.', 'success'); ambilDataPengguna(); } async function formPengguna(mode, id_user = '') { let u = {}; if (mode === 'edit') { const { data, error } = await supabaseClient.from('pengguna') .select('id_user, username, nama_lengkap, role, akses_menu, status_aktif').eq('id_user', id_user).single(); if (error || !data) return NotifikasiKecil('Pegawai tidak ditemukan', 'error'); u = data; } let akses = Array.isArray(u.akses_menu) ? u.akses_menu : []; if (u.role === 'Owner') akses = [...SEMUA_MENU]; let diriSendiri = mode === 'edit' && window.currentUser && window.currentUser.id_user === u.id_user; const { value: formValues } = await Swal.fire({ title: mode === 'tambah' ? 'Tambah Pegawai Baru' : 'Ubah Data Pegawai', width: 500, html: `
Nama Lengkap
Username (untuk login)

Huruf kecil, angka, titik, strip. ${mode==='edit' ? 'Username tidak bisa diubah.' : 'Pegawai login dengan username ini.'}

Role / Jabatan
${mode==='tambah' ? 'PIN / Password' : 'PIN Baru (opsional)'}

Kasir: 6 digit angka. Owner/Admin: minimal 8 karakter.

${diriSendiri ? '

Anda tidak dapat mengubah role atau menu akun sendiri.

' : ''}
Hak Akses Menu (Centang)
`, didOpen: () => { window.onGantiRole = (role) => { if (role === 'Owner') document.querySelectorAll('.cb-akses').forEach(cb => { cb.checked = true; }); const hint = document.getElementById('swal-hint-pin'); hint.innerText = role === 'Kasir' ? 'Kasir: 6 digit angka.' : 'Owner/Admin: minimal 8 karakter.'; }; }, focusConfirm: false, showCancelButton: true, confirmButtonText: 'Simpan', preConfirm: () => { let nama = document.getElementById('swal-namaUser').value.trim(); let username = document.getElementById('swal-usernameUser').value.trim().toLowerCase(); let role = document.getElementById('swal-roleUser').value; let pin = document.getElementById('swal-pinUser').value; if (!nama) { Swal.showValidationMessage('Nama lengkap wajib diisi'); return false; } if (mode === 'tambah') { if (!/^[a-z0-9._-]{3,30}$/.test(username)) { Swal.showValidationMessage('Username 3-30 karakter: huruf kecil, angka, titik, strip'); return false; } if (!pin) { Swal.showValidationMessage('PIN/Password wajib diisi'); return false; } } if (pin) { if (role === 'Kasir' && !/^\d{6}$/.test(pin)) { Swal.showValidationMessage('PIN Kasir harus tepat 6 digit angka'); return false; } if (role !== 'Kasir' && pin.length < 8) { Swal.showValidationMessage('Password Owner/Admin minimal 8 karakter'); return false; } } let aksesTerpilih = Array.from(document.querySelectorAll('.cb-akses:checked')).map(el => el.value); if (role === 'Owner') aksesTerpilih = [...SEMUA_MENU]; if (aksesTerpilih.length === 0) { Swal.showValidationMessage('Pilih minimal satu menu akses'); return false; } return { nama_lengkap: nama, username, role, pin, akses_menu: aksesTerpilih }; } }); if (!formValues) return; Swal.fire({ title: 'Menyimpan...', allowOutsideClick: false, didOpen: () => Swal.showLoading() }); let payload; if (mode === 'tambah') { payload = { aksi: 'buat', username: formValues.username, nama_lengkap: formValues.nama_lengkap, role: formValues.role, pin: formValues.pin, akses_menu: formValues.akses_menu }; } else { payload = { aksi: 'ubah', id_user: u.id_user, nama_lengkap: formValues.nama_lengkap }; if (!diriSendiri) { payload.role = formValues.role; payload.akses_menu = formValues.akses_menu; } if (formValues.pin) payload.pin = formValues.pin; } const hasil = await panggilFungsiPegawai(payload); if (!hasil.ok) return Swal.fire('Gagal', hasil.pesan, 'error'); Swal.fire('Berhasil!', 'Data pegawai tersimpan.', 'success'); ambilDataPengguna(); } // =========================================================== // LOGIN, LOGOUT & SESI (Supabase Auth) // =========================================================== window.currentUser = null; async function prosesLogin() { const username = document.getElementById('input-username').value.trim().toLowerCase(); const pin = document.getElementById('input-pin').value; if (!username || !pin) return NotifikasiKecil('Nama Pengguna dan PIN wajib diisi!', 'warning'); Swal.fire({ title: 'Memeriksa...', allowOutsideClick: false, didOpen: () => Swal.showLoading() }); const { error } = await supabaseClient.auth.signInWithPassword({ email: username + '@' + EMAIL_DOMAIN, password: pin }); if (error) { document.getElementById('input-pin').value = ''; return Swal.fire('Akses Ditolak', 'Nama Pengguna atau PIN tidak sesuai.', 'error'); } // Profil dimuat oleh onAuthStateChange const ok = await muatProfilDanTampilkan(); if (ok) Swal.close(); } // Ambil profil pegawai milik user yang sedang login, lalu tampilkan UI async function muatProfilDanTampilkan() { const { data: { user } } = await supabaseClient.auth.getUser(); if (!user) { tampilkanLayarLogin(); return false; } const { data: profil, error } = await supabaseClient.from('pengguna') .select('id_user, username, nama_lengkap, role, akses_menu, status_aktif').eq('id_user', user.id).single(); if (error || !profil) { await supabaseClient.auth.signOut(); tampilkanLayarLogin(); Swal.fire('Akun Belum Siap', 'Akun login Anda belum memiliki profil pegawai. Hubungi Owner.', 'error'); return false; } if (!profil.status_aktif) { await supabaseClient.auth.signOut(); tampilkanLayarLogin(); Swal.fire('Akses Diblokir', 'Akun Anda sedang dinonaktifkan.', 'error'); return false; } terapkanSesiAktif(profil); return true; } function prosesLogout() { Swal.fire({ title: 'Keluar dari Sistem?', text: "Anda harus memasukkan Nama dan PIN kembali untuk masuk.", icon: 'warning', showCancelButton: true, confirmButtonColor: '#ef4444', confirmButtonText: 'Ya, Keluar' }).then(async (result) => { if (!result.isConfirmed) return; await supabaseClient.auth.signOut(); window.currentUser = null; document.getElementById('input-username').value = ''; document.getElementById('input-pin').value = ''; tampilkanLayarLogin(); }); } function tampilkanLayarLogin() { const layar = document.getElementById('layar-login'); layar.classList.remove('hidden'); layar.style.opacity = '1'; document.getElementById('input-username').focus(); } function terapkanSesiAktif(profil) { window.currentUser = profil; document.getElementById('nama-user').innerText = profil.nama_lengkap; document.getElementById('role-user').innerText = profil.role; document.getElementById('avatar-user').innerText = (profil.nama_lengkap || '?').charAt(0).toUpperCase(); let layar = document.getElementById('layar-login'); layar.style.opacity = '0'; setTimeout(() => { layar.classList.add('hidden'); }, 500); let akses = Array.isArray(profil.akses_menu) ? [...profil.akses_menu] : []; if (profil.role === 'Owner') akses = [...SEMUA_MENU]; else if (akses.length === 0) akses = ['layar-kasir']; let layarPertama = ''; SEMUA_MENU.forEach(id => { let el = document.getElementById('menu-' + id); if (!el) return; if (akses.includes(id)) { el.classList.remove('hidden'); el.classList.add('flex'); if (layarPertama === '') layarPertama = id; } else { el.classList.remove('flex'); el.classList.add('hidden'); } }); if (akses.includes('layar-kasir')) { muatLayarKasir().then(() => { if (layarPertama) gantiLayar(layarPertama); }); } else if (layarPertama) { gantiLayar(layarPertama); } } // =========================================================== // MODUL PENGELUARAN (RESTOK & OPEX) // =========================================================== window.switchTabPengeluaran = function(tabId) { document.getElementById('tab-pembelian').classList.add('hidden'); document.getElementById('tab-operasional').classList.add('hidden'); document.getElementById('btn-tab-pembelian').className = 'px-4 py-2 font-bold text-slate-500 hover:text-slate-700 transition-colors'; document.getElementById('btn-tab-operasional').className = 'px-4 py-2 font-bold text-slate-500 hover:text-slate-700 transition-colors'; document.getElementById(tabId).classList.remove('hidden'); document.getElementById('btn-' + tabId).className = 'px-4 py-2 font-bold text-amber-600 border-b-2 border-amber-500 transition-colors'; }; async function ambilDataKasKeluar() { const { data: dataBeli, error: eBeli } = await supabaseClient.from('pembelian_bahan') .select('*, master_bahan_baku(nama_bahan, satuan)').order('tanggal', { ascending: false }).limit(100); let htmlBeli = ''; if (eBeli) htmlBeli = `${esc(pesanError(eBeli))}`; else if (dataBeli && dataBeli.length > 0) { dataBeli.forEach(d => { let tgl = new Date(d.tanggal).toLocaleString('id-ID', { dateStyle: 'short', timeStyle: 'short' }); htmlBeli += ` ${esc(tgl)} ${esc(d.master_bahan_baku?.nama_bahan || '(dihapus)')} ${(d.qty||0).toLocaleString('id-ID')} ${esc(d.master_bahan_baku?.satuan || '')} Rp ${(d.harga_total||0).toLocaleString('id-ID')} ${esc(d.keterangan || '-')} `; }); } else htmlBeli = 'Belum ada data pembelian'; document.getElementById('tabel-pembelian').innerHTML = htmlBeli; const { data: dataOpex, error: eOpex } = await supabaseClient.from('biaya_operasional') .select('*').order('tanggal', { ascending: false }).limit(100); let htmlOpex = ''; if (eOpex) htmlOpex = `${esc(pesanError(eOpex))}`; else if (dataOpex && dataOpex.length > 0) { dataOpex.forEach(d => { let tgl = new Date(d.tanggal).toLocaleString('id-ID', { dateStyle: 'short', timeStyle: 'short' }); htmlOpex += ` ${esc(tgl)} ${esc(d.kategori)} Rp ${(d.nominal||0).toLocaleString('id-ID')} ${esc(d.keterangan || '-')} `; }); } else htmlOpex = 'Belum ada pengeluaran operasional'; document.getElementById('tabel-operasional').innerHTML = htmlOpex; } async function formPembelian() { Swal.fire({ title: 'Memuat Bahan...', allowOutsideClick: false, didOpen: () => Swal.showLoading() }); const { data: dbBahan } = await supabaseClient.from('master_bahan_baku') .select('id_bahan, nama_bahan, satuan').eq('aktif', true).order('nama_bahan'); let opsi = (dbBahan || []).map(b => ``).join(''); const { value: formValues } = await Swal.fire({ title: 'Catat Restok Bahan Baku', html: `
Pilih Bahan yang Dibeli
Kuantitas (Qty)
Total Harga Beli (Rp)
Keterangan / Supplier (Opsional)
`, focusConfirm: false, showCancelButton: true, confirmButtonText: 'Simpan Pembelian', confirmButtonColor: '#059669', preConfirm: () => { let id_bahan = document.getElementById('swal-beli-bahan').value; let qtyRaw = document.getElementById('swal-beli-qty').value.replace(/\./g, ''); let hargaRaw = document.getElementById('swal-beli-harga').value.replace(/\./g, ''); if (!id_bahan || !qtyRaw || !hargaRaw) { Swal.showValidationMessage('Bahan, Kuantitas, dan Harga Beli wajib diisi!'); return false; } return { id_bahan, qty: parseInt(qtyRaw) || 0, harga_total: parseInt(hargaRaw) || 0, keterangan: document.getElementById('swal-beli-ket').value }; } }); if (formValues) { Swal.fire({ title: 'Menyimpan & Menambah Stok...', allowOutsideClick: false, didOpen: () => Swal.showLoading() }); const { error } = await supabaseClient.from('pembelian_bahan').insert([formValues]); if (error) return Swal.fire('Error', pesanError(error, 'Gagal mencatat pembelian'), 'error'); Swal.fire('Berhasil!', 'Pembelian dicatat & Stok otomatis bertambah!', 'success'); ambilDataKasKeluar(); } } async function formOperasional() { const { value: formValues } = await Swal.fire({ title: 'Catat Pengeluaran Operasional', html: `
Kategori Biaya
Nominal Pengeluaran (Rp)
Keterangan Detail
`, focusConfirm: false, showCancelButton: true, confirmButtonText: 'Simpan Pengeluaran', confirmButtonColor: '#2563eb', preConfirm: () => { let nominalRaw = document.getElementById('swal-opex-nominal').value.replace(/\./g, ''); if (!nominalRaw) { Swal.showValidationMessage('Nominal wajib diisi!'); return false; } return { kategori: document.getElementById('swal-opex-kategori').value, nominal: parseInt(nominalRaw) || 0, keterangan: document.getElementById('swal-opex-ket').value }; } }); if (formValues) { Swal.fire({ title: 'Menyimpan...', allowOutsideClick: false, didOpen: () => Swal.showLoading() }); const { error } = await supabaseClient.from('biaya_operasional').insert([formValues]); if (error) return Swal.fire('Error', pesanError(error, 'Gagal mencatat pengeluaran'), 'error'); Swal.fire('Berhasil!', 'Pengeluaran berhasil dicatat.', 'success'); ambilDataKasKeluar(); } } // =========================================================== // PENGATURAN OUTLET (satu baris per outlet; tanpa .eq('id',1)) // =========================================================== window.pengaturanOutlet = {}; async function muatPengaturan() { const { data, error } = await supabaseClient.from('konfigurasi_sistem').select('*').limit(1).maybeSingle(); if (error) { NotifikasiKecil(pesanError(error), 'error'); return; } if (!data) return; window.pengaturanOutlet = data; document.getElementById('nama-toko').innerText = data.nama_usaha && data.nama_usaha !== '-' ? data.nama_usaha : "POS MODULAR"; const imgLogo = document.getElementById('logo-toko-sidebar'); const textLogo = document.getElementById('logo-toko-text'); if (data.logo_url && data.logo_url.startsWith('http')) { imgLogo.src = data.logo_url; imgLogo.classList.remove('hidden'); textLogo.classList.add('hidden'); } else { textLogo.innerText = (data.logo_url && data.logo_url !== '-') ? data.logo_url : "๐Ÿ”"; textLogo.classList.remove('hidden'); imgLogo.classList.add('hidden'); } let formLayar = document.getElementById('layar-pengaturan'); if (formLayar) { document.getElementById('input-nama-brand').value = data.nama_usaha && data.nama_usaha !== '-' ? data.nama_usaha : ''; document.getElementById('input-alamat-brand').value = data.alamat && data.alamat !== '-' ? data.alamat : ''; document.getElementById('input-ig-brand').value = data.instagram && data.instagram !== '-' ? data.instagram : ''; document.getElementById('input-ket-brand').value = data.keterangan && data.keterangan !== '-' ? data.keterangan : ''; let preview = document.getElementById('preview-logo-pengaturan'); if (data.logo_url && data.logo_url.startsWith('http')) preview.innerHTML = ``; else preview.innerHTML = esc((data.logo_url && data.logo_url !== '-') ? data.logo_url : "๐Ÿ”"); } } async function simpanPengaturan() { Swal.fire({ title: 'Menyimpan...', allowOutsideClick: false, didOpen: () => Swal.showLoading() }); let finalGambarUrl = window.pengaturanOutlet.logo_url; const fileInput = document.getElementById('input-logo-pengaturan').files[0]; if (fileInput) { if (fileInput.size > 2 * 1024 * 1024) return Swal.fire('Error', 'Ukuran logo maksimal 2 MB', 'error'); const fileExt = (fileInput.name.split('.').pop() || 'png').toLowerCase(); const fileName = `logo_${Date.now()}.${fileExt}`; const { error: uploadError } = await supabaseClient.storage.from('foto_produk').upload(fileName, fileInput); if (uploadError) return Swal.fire('Error', pesanError(uploadError, 'Gagal upload logo'), 'error'); finalGambarUrl = supabaseClient.storage.from('foto_produk').getPublicUrl(fileName).data.publicUrl; } let payload = { nama_usaha: document.getElementById('input-nama-brand').value, alamat: document.getElementById('input-alamat-brand').value, instagram: document.getElementById('input-ig-brand').value, keterangan: document.getElementById('input-ket-brand').value, logo_url: finalGambarUrl || '๐Ÿ”' }; // Update baris konfigurasi milik outlet ini (via id yang sudah dimuat) const idKonfig = window.pengaturanOutlet.id; const { error } = await supabaseClient.from('konfigurasi_sistem').update(payload).eq('id', idKonfig); if (error) return Swal.fire('Error', pesanError(error, 'Gagal menyimpan pengaturan'), 'error'); Swal.fire('Berhasil!', 'Identitas brand tersimpan.', 'success'); muatPengaturan(); } // =========================================================== // PEMUAT AWAL // =========================================================== window.onload = async function() { // Pulihkan sesi jika masih ada (Supabase menyimpan token di localStorage) const { data: { session } } = await supabaseClient.auth.getSession(); if (session) { await muatPengaturan(); // identitas brand & logo await muatProfilDanTampilkan(); } else { tampilkanLayarLogin(); } };